/*
 * Please refer to the LICENSE file in the root directory of the project.
 * https://github.com/kubesphere/kubesphere/blob/master/LICENSE
 */

package quota

import (
	"context"
	"net/http"
	"sort"
	"sync"

	kscontroller "kubesphere.io/kubesphere/pkg/controller"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilwait "k8s.io/apimachinery/pkg/util/wait"
	admissionapi "k8s.io/apiserver/pkg/admission"
	"k8s.io/apiserver/pkg/authentication/user"
	"k8s.io/klog/v2"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	"kubesphere.io/kubesphere/kube/pkg/quota/v1"
	"kubesphere.io/kubesphere/kube/pkg/quota/v1/generic"
	"kubesphere.io/kubesphere/kube/pkg/quota/v1/install"
	"kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota"
	resourcequotaapi "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota/apis/resourcequota"
	"kubesphere.io/kubesphere/pkg/scheme"
)

const (
	numEvaluatorThreads = 10
)

type ResourceQuotaAdmission struct {
	client client.Client

	decoder webhook.AdmissionDecoder

	lockFactory LockFactory

	// these are used to create the evaluator
	registry quota.Registry

	init      sync.Once
	evaluator resourcequota.Evaluator
}

const webhookName = "resource-quota-webhook"

func (w *Webhook) Name() string {
	return webhookName
}

var _ kscontroller.Controller = &Webhook{}

type Webhook struct {
}

func (w *Webhook) SetupWithManager(mgr *kscontroller.Manager) error {
	resourceQuotaAdmission := &ResourceQuotaAdmission{
		client:      mgr.GetClient(),
		lockFactory: NewDefaultLockFactory(),
		decoder:     admission.NewDecoder(mgr.GetScheme()),
		registry:    generic.NewRegistry(install.NewQuotaConfigurationForAdmission().Evaluators()),
	}
	mgr.GetWebhookServer().Register("/validate-quota-kubesphere-io-v1alpha2", &webhook.Admission{Handler: resourceQuotaAdmission})
	return nil
}

func (r *ResourceQuotaAdmission) Handle(ctx context.Context, req webhook.AdmissionRequest) webhook.AdmissionResponse {
	// ignore all operations that correspond to sub-resource actions
	if len(req.RequestSubResource) != 0 {
		return webhook.Allowed("")
	}
	// ignore cluster level resources
	if len(req.Namespace) == 0 {
		return webhook.Allowed("")
	}

	r.init.Do(func() {
		resourceQuotaAccessor := newQuotaAccessor(r.client)
		r.evaluator = resourcequota.NewQuotaEvaluator(resourceQuotaAccessor, install.DefaultIgnoredResources(), r.registry, r.lockAquisition, &resourcequotaapi.Configuration{}, numEvaluatorThreads, utilwait.NeverStop)
	})

	attributesRecord, err := convertToAdmissionAttributes(req)
	if err != nil {
		klog.Error(err)
		return webhook.Errored(http.StatusBadRequest, err)
	}

	if err := r.evaluator.Evaluate(attributesRecord); err != nil {
		if errors.IsForbidden(err) {
			klog.Info(err)
			return webhook.Denied(err.Error())
		}
		klog.Error(err)
		return webhook.Errored(http.StatusInternalServerError, err)
	}

	return webhook.Allowed("")
}

type ByName []corev1.ResourceQuota

func (v ByName) Len() int           { return len(v) }
func (v ByName) Swap(i, j int)      { v[i], v[j] = v[j], v[i] }
func (v ByName) Less(i, j int) bool { return v[i].Name < v[j].Name }

func (r *ResourceQuotaAdmission) lockAquisition(quotas []corev1.ResourceQuota) func() {
	var locks []sync.Locker

	// acquire the locks in alphabetical order because I'm too lazy to think of something clever
	sort.Sort(ByName(quotas))
	for _, q := range quotas {
		lock := r.lockFactory.GetLock(string(q.UID))
		lock.Lock()
		locks = append(locks, lock)
	}

	return func() {
		for i := len(locks) - 1; i >= 0; i-- {
			locks[i].Unlock()
		}
	}
}

func convertToAdmissionAttributes(req admission.Request) (admissionapi.Attributes, error) {
	var err error
	var object runtime.Object
	if len(req.Object.Raw) > 0 {
		object, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Object.Raw, nil, nil)
		if err != nil {
			return nil, err
		}
	}

	var oldObject runtime.Object
	if len(req.OldObject.Raw) > 0 {
		oldObject, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.OldObject.Raw, nil, nil)
		if err != nil {
			klog.Error(err)
			return nil, err
		}
	}

	var operationOptions runtime.Object
	if len(req.Options.Raw) > 0 {
		operationOptions, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Options.Raw, nil, nil)
		if err != nil {
			klog.Error(err)
			return nil, err
		}
	}

	extras := map[string][]string{}
	for k, v := range req.UserInfo.Extra {
		extras[k] = v
	}

	attributesRecord := admissionapi.NewAttributesRecord(object,
		oldObject,
		schema.GroupVersionKind{
			Group:   req.RequestKind.Group,
			Version: req.RequestKind.Version,
			Kind:    req.RequestKind.Kind,
		},
		req.Namespace,
		req.Name,
		schema.GroupVersionResource{
			Group:    req.RequestResource.Group,
			Version:  req.RequestResource.Version,
			Resource: req.RequestResource.Resource,
		},
		req.SubResource,
		admissionapi.Operation(req.Operation),
		operationOptions,
		*req.DryRun,
		&user.DefaultInfo{
			Name:   req.UserInfo.Username,
			UID:    req.UserInfo.UID,
			Groups: req.UserInfo.Groups,
			Extra:  extras,
		})
	return attributesRecord, nil
}
